﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Collections;

namespace SwiftMVVM.ViewModel
{
    /// <summary>
    /// DataErrorInfoSupport is a basic helper class that fires off all of the validators
    /// for a given property, object, or recursively down an object model
    /// </summary>
    public static class DataErrorInfoSupport
    {

        /// <summary>
        /// ValidateProperty looks at the object property, checks it for validation attributes and
        /// runs each validation attribute against the current value
        /// </summary>
        /// <param name="value">The current object</param>
        /// <param name="propertyName">The property to check</param>
        /// <param name="validationResults">The results as a set</param>
        /// <returns>True if the property is valid, false otherwise</returns>
        private static bool ValidateProperty(object value, string propertyName, ICollection<ValidationResult> validationResults)
        {
            if (value == null) { throw new ArgumentNullException("value"); }
            if (string.IsNullOrWhiteSpace(propertyName)) { throw new ArgumentNullException("memberName"); }
            if (validationResults == null) { throw new ArgumentNullException("validationResults"); }

            var propertyInfo = value.GetType().GetProperty(propertyName);

            var validationAttributes = (ValidationAttribute[])propertyInfo.
                GetCustomAttributes(typeof(ValidationAttribute), true);

            var propertyValue = propertyInfo.GetValue(value, null);
            var invalidValidationAttribute = validationAttributes.FirstOrDefault(
                validationAttribute => !validationAttribute.IsValid(propertyValue));
            
            if (invalidValidationAttribute != null)
            {
                validationResults.Add(new ValidationResult(
                    invalidValidationAttribute.FormatErrorMessage(propertyInfo.Name)));
                return false;
            }
            return true;
        }

        /// <summary>
        /// TryValidateObject essentially calls ValidateProperty for every public property
        /// marked with ValidationAttributes on the object passed in
        /// </summary>
        /// <param name="value">The current object</param>
        /// <param name="validationResults">The results as a set</param>
        /// <returns>True if the property is valid, false otherwise</returns>
        private static bool TryValidateObject(object value, ICollection<ValidationResult> validationResults)
        {
            if (value == null) { throw new ArgumentNullException("value"); }
            if (validationResults == null) { throw new ArgumentNullException("validationResults"); }

            try
            {
                return !value.GetType().GetProperties().Any(
                    propertyInfo => !ValidateProperty(value, propertyInfo.Name, validationResults));
            }
            catch { return false; }
        }


        /// <summary>
        /// Returns a formatted error message for the object as a whole, or the member passed in
        /// </summary>
        /// <param name="instance">The object to check</param>
        /// <param name="propertyName">Optional Property Name</param>
        /// <returns>A formatted string detailing the errors on the object</returns>
        public static string GetError(object instance, string propertyName = "")
        {
            var validationResults = new List<ValidationResult>();
            if (string.IsNullOrWhiteSpace(propertyName))
            {
                TryValidateObject(instance, validationResults);
            }
            else
            {
                try
                {
                    ValidateProperty(instance, propertyName, validationResults);
                }
                catch { }
            }

            var errorBuilder = new StringBuilder();
            foreach (var validationResult in validationResults)
            {
                errorBuilder.AppendLine(validationResult.ErrorMessage);
            }

            return errorBuilder.ToString();
        }

        /// <summary>
        /// Attempts to recursively validate an entire object model. 
        /// It will return the first error encountered
        /// </summary>
        /// <param name="value">The object to check</param>
        /// <param name="validationResults">The results as a set</param>
        /// <returns>True if the entire is valid, false otherwise</returns>
        public static bool ValidateModel(IDataErrorInfo value, List<ValidationResult> validationResults)
        {
            if (value == null) { throw new ArgumentNullException("value"); }
            if (validationResults == null) { throw new ArgumentNullException("validationResults"); }

            var validatedObjects = new HashSet<object>();
            return ValidateModel(validatedObjects, value, validationResults);
        }


        /// <summary>
        /// Validates an entire model returning the first error encountered
        /// </summary>
        /// <param name="root">Root object</param>
        /// <returns>First error encountered</returns>
        private static bool ValidateModel(HashSet<object> validatedObjects, IDataErrorInfo value, List<ValidationResult> validationResults)
        {
            validatedObjects.Add(value);
            var result = TryValidateObject(value, validationResults);
            if (result)
            {
                //He validated, so start checking his properties and sub collections
                //for objects that implement IDataErrorInfo

                foreach (var propertyValue in value.GetType().GetProperties()
                     .Where(propertyInfo => 
                        typeof(IDataErrorInfo).IsAssignableFrom(propertyInfo.PropertyType) ||
                        typeof(ICollection).IsAssignableFrom(propertyInfo.PropertyType))
                     .Select(propertyInfo => propertyInfo.GetValue(value, null)))
                {
                    var collection = propertyValue as ICollection;
                    if (collection != null)
                    {
                        if(collection.OfType<IDataErrorInfo>()
                            .Where(collectionItemDataErrorInfo => !validatedObjects.Contains(collectionItemDataErrorInfo))
                            .Any(collectionItemDataErrorInfo => !ValidateModel(validatedObjects, collectionItemDataErrorInfo, validationResults)))
                        {
                            //This guy did not validate, return out.
                            return false;
                        }
                    }

                    var dataErrorInfo = propertyValue as IDataErrorInfo;
                    if (dataErrorInfo != null &&
                        !validatedObjects.Contains(dataErrorInfo) &&
                        !ValidateModel(validatedObjects, dataErrorInfo, validationResults))
                    {
                        //This guy did not validate, return out.
                        return false;
                    }
                }  
            }
            return result;
        }
    }
}
